﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BobcatUtility;
using CNUtility;
using System.ServiceModel;
using CNShapes;
using System.ServiceModel.Channels;
using CNStreaming;

namespace CNNetworking
{
    /// <summary>
    /// This class handles the client end of the network communication. It can
    /// send messages via the IClientToServer interfaces, and receive callbacks
    /// via the IServerToClient callback-interface.
    /// 
    /// Communication between the client and the server uses WCF.
    /// 
    /// This class is a singleton.
    /// </summary>
    public class NetworkingClient : IServerToClient
    {
        #region Events

        /// <summary>
        /// Data passed with the heartbeat event.
        /// </summary>
        public class HeartbeatArgs : EventArgs
        {
            public IList<string> ClientNames { get; set; }
        }
        /// <summary>
        /// Event raised when we receive a heartbeat from the server.
        /// </summary>
        public event EventHandler<HeartbeatArgs> HeartbeatEvent;

        /// <summary>
        /// Data passed with the client-list updated event.
        /// </summary>
        public class ClientListUpdatedArgs : EventArgs
        {
            public IList<string> ClientNames { get; set; }
        }
        /// <summary>
        /// Event raised when the list of clients connected to the current session changes.
        /// </summary>
        public event EventHandler<ClientListUpdatedArgs> ClientListUpdatedEvent;

        #endregion

        #region Public properties

        /// <summary>
        /// Property to get the unique (GUID) ID of this client.
        /// </summary>
        public Guid ClientID
        {
            get { return m_ClientID; }
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Singleton's getInstance method.
        /// </summary>
        public static NetworkingClient getInstance()
        {
            if (m_Instance == null)
            {
                m_Instance = new NetworkingClient();
            }
            return m_Instance;
        }

        /// <summary>
        /// Returns the connection-settings for this client.
        /// </summary>
        public ConnectionSettings getClientConnectionSettings()
        {
            ConnectionSettings connectionSettings = new ConnectionSettings();
            connectionSettings.FirstName = UserPreferences.getPropertyBag().getString("UserInfo.FirstName");
            connectionSettings.LastName = UserPreferences.getPropertyBag().getString("UserInfo.LastName");
            connectionSettings.HostName = Environment.MachineName;
            return connectionSettings;
        }

        /// <summary>
        /// We connect to the server whose settings are passed in.
        /// </summary>
        public void connect(ConnectionSettings serverSettings)
        {
            try
            {
                // We create the endpoint address to connect to...
                int iPort = UserPreferences.getPropertyBag().getInt("Networking.Port");
                string strEndpoint = "net.tcp://" + serverSettings.HostName + ":" + iPort.ToString() + "/Whiteboard/";
                Uri uri = new Uri(strEndpoint);

                // We need to create an "identity" for security reasons. (I don't really understand this,
                // but if you  don't do it you can't connect between machines on a LAN.)
                SpnEndpointIdentity identity = new SpnEndpointIdentity("");

                // We create the endpoint and connect...
                EndpointAddress serverAddress = new EndpointAddress(uri, identity, new AddressHeaderCollection());
                m_Server = new DuplexChannelFactory<IClientToServer>(this, "DefaultEndpoint", serverAddress).CreateChannel();

                // We notify the server that we want to connect, and provide our details...
                ConnectionSettings connectionSettings = getClientConnectionSettings();
                string strName = connectionSettings.FirstName + " " + connectionSettings.LastName;
                m_Server.connect(m_ClientID, strName);
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
        }

        /// <summary>
        /// We send a message to the server to tell it that we are disconnecting.
        /// </summary>
        public void disconnect()
        {
            if (m_Server == null)
            {
                return;
            }

            try
            {
                m_Server.disconnect(m_ClientID);
                m_Server = null;
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
        }

        #endregion

        #region IServerToClient members

        /// <summary>
        /// Called by the server to tell us that it is disconnecting.
        /// </summary>
        public void onDisconnected()
        {
            Utility.logEntry();

            try
            {
                m_Server = null;
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
            finally
            {
                Utility.logExit();
            }
        }

        /// <summary>
        /// Called when the server sends us a heartbeat. This is a two-way message (though
        /// it doesn't send any data back). Just returning successfully is enough to let
        /// the server know we're still alive.
        /// </summary>
        public void onHeartbeat()
        {
            Utility.logEntry();

            try
            {
                Utility.raiseEvent(HeartbeatEvent, this, new HeartbeatArgs());
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
            finally
            {
                Utility.logExit();
            }
        }

        /// <summary>
        /// Called when the client list (i.e. the list of people connected to the same
        /// session) is updated.
        /// </summary>
        public void onClientListUpdated(IList<string> clientNames)
        {
            Utility.logEntry();

            try
            {
                Utility.raiseEvent(ClientListUpdatedEvent, this, new ClientListUpdatedArgs { ClientNames = clientNames });
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
            finally
            {
                Utility.logExit();
            }
        }

        /// <summary>
        /// Called by the server to request that we share our server-list with it.
        /// </summary>
        public void requestAddressBook()
        {
            Utility.logEntry();

            try
            {
                IList<ConnectionSettings> results;

                // We check if wee are set up to share our settings...
                bool bShareAddressBook = UserPreferences.getPropertyBag().getBool("Networking.ShareAddressBook");
                if (bShareAddressBook == true)
                {
                    // We are sharing the list, so we get it from the preferences...
                    results = UserPreferences.getPropertyBag().getList<ConnectionSettings>("Networking.AddressBook");
                }
                else
                {
                    // We are not sharing the list, so we return an empty list...
                    results = new List<ConnectionSettings>();
                }
                m_Server.sendAddressBook(results);
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
            finally
            {
                Utility.logExit();
            }
        }

        /// <summary>
        /// Called when we receive a new set of connection settings. These will be the
        /// settings from one of the other clients in the system. We merge them with our
        /// current set.
        /// </summary>
        public void onNewAddressBook(IList<ConnectionSettings> connectionSettings)
        {
            Utility.logEntry();

            try
            {
                // We see if this list contains any settings that we do not already hold. If it
                // does, we add them to our list.

                // To do this more efficiently, we put the existing data into a dictionary (we'd
                // use a set if C# had one!)...
                IList<ConnectionSettings> listCurrentAddressBook = UserPreferences.getPropertyBag().getList<ConnectionSettings>("Networking.AddressBook");
                IDictionary<string, bool> setCurrentConnectionSettings = new Dictionary<string, bool>();
                foreach (ConnectionSettings cs in listCurrentAddressBook)
                {
                    setCurrentConnectionSettings.Add(cs.ToString(), true);
                }

                // Now we look through the collection of connection-settings passed in
                // to see if any of them are new...
                foreach (ConnectionSettings cs in connectionSettings)
                {
                    if (setCurrentConnectionSettings.ContainsKey(cs.ToString()) == false)
                    {
                        // We don't already have this connection-setting, so
                        // we'll add it to our collection...
                        listCurrentAddressBook.Add(cs);
                    }
                }

                // We store the new list back in the properties...
                UserPreferences.getPropertyBag().setList("Networking.AddressBook", listCurrentAddressBook);
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
            finally
            {
                Utility.logExit();
            }
        }

        /// <summary>
        /// Called by the server to tell us that a new session has started.
        /// </summary>
        public void onNewSession(Guid sessionID, Guid originatingClientID)
        {
            Utility.logEntry();

            try
            {
                // We clear the current session and set its ID to the
                // one passed in...
                Session session = Session.getInstance();
                session.newSession(sessionID, getActionOrigin(originatingClientID));
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
            finally
            {
                Utility.logExit();
            }
        }

        /// <summary>
        /// Called by the server when a page is added to the session.
        /// </summary>
        public void onNewPage(Guid sessionID, Guid pageID, Guid originatingClientID)
        {
            Utility.logEntry();

            try
            {
                checkSession(sessionID);

                // We add a page with the ID passed in to our session...
                Page page = Session.getInstance().addPage(pageID, getActionOrigin(originatingClientID));
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
            finally
            {
                Utility.logExit();
            }
        }

        /// <summary>
        /// Called by the server to give us the ID of the new active page.
        /// </summary>
        public void onActivePageChanged(Guid sessionID, Guid pageID, Guid originatingClientID)
        {
            Utility.logEntry();

            try
            {
                checkSession(sessionID);
                Session.getInstance().setActivePageID(pageID, getActionOrigin(originatingClientID));
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
            finally
            {
                Utility.logExit();
            }
        }

        /// <summary>
        /// Called by the server when a shape is added to the specified page.
        /// </summary>
        public void onNewShape(Guid sessionID, Guid pageID, byte[] shapeData, Guid originatingClientID)
        {
            Utility.logEntry();

            try
            {
                checkSession(sessionID);

                // We find the page with the ID passed in and add the shape to it...
                Page page = Session.getInstance().getPage(pageID);
                Shape shape = StreamHelper.convertFromByteArray<Shape>(shapeData);
                page.addShape(shape, getActionOrigin(originatingClientID));
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
            finally
            {
                Utility.logExit();
            }
        }

        /// <summary>
        /// Called by the server when a shape has been updated on the specified page.
        /// </summary>
        public void onUpdatedShape(Guid sessionID, Guid pageID, byte[] shapeData, Guid originatingClientID)
        {
            Utility.logEntry();

            try
            {
                checkSession(sessionID);

                // We find the page with the ID passed in and add the shape to it...
                Page page = Session.getInstance().getPage(pageID);
                Shape shape = StreamHelper.convertFromByteArray<Shape>(shapeData);
                page.removeShape(shape.ID, getActionOrigin(originatingClientID));
                page.addShape(shape, getActionOrigin(originatingClientID));
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
            finally
            {
                Utility.logExit();
            }
        }

        /// <summary>
        /// Called by the server when another client has removed a shape from
        /// the page and session passed in.
        /// </summary>
        public void onRemovedShape(Guid sessionID, Guid pageID, Guid shapeID, Guid originatingClientID)
        {
            Utility.logEntry();

            try
            {
                checkSession(sessionID);

                // We find the page with the ID passed in and remove the shape from it...
                Page page = Session.getInstance().getPage(pageID);
                page.removeShape(shapeID, getActionOrigin(originatingClientID));
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
            finally
            {
                Utility.logExit();
            }
        }

        #endregion

        #region Handlers for events from the Model

        /// <summary>
        /// Called when a shape has been removed.
        /// </summary>
        private void onShapeRemoved(object sender, Session.Args e)
        {
            if (serverUpdateRequired(e.ActionOrigin) == false) return;
            Utility.logEntry();

            try
            {
                Session session = (Session)sender;
                m_Server.shapeRemoved(m_ClientID, session.ID, e.Page.ID, e.Shape.ID);
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
            finally
            {
                Utility.logExit();
            }
        }

        /// <summary>
        /// Called when a shape has been added or edited.
        /// </summary>
        private void onShapeUpdated(object sender, Session.Args e)
        {
            if (serverUpdateRequired(e.ActionOrigin) == false) return;
            Utility.logEntry();

            try
            {
                Session session = (Session)sender;
                byte[] shapeData = StreamHelper.convertToByteArray(e.Shape);
                m_Server.shapeEdited(m_ClientID, session.ID, e.Page.ID, shapeData);
            }
            catch (Exception ex)
            {
                Logger.log(ex.Message, Logger.Level.WARN);
            }
            finally
            {
                Utility.logExit();
            }
        }

		/// <summary>
		/// Called when a new session is started. We notify the server.
		/// </summary>
		private void onNewSession(object sender, Session.Args e)
		{
            if (serverUpdateRequired(e.ActionOrigin) == false) return;
            Utility.logEntry();

			try
			{
				Session session = (Session)sender;
				m_Server.newSession(m_ClientID, session.ID);
			}
			catch (Exception ex)
			{
				Logger.log(ex.Message, Logger.Level.WARN);
			}
            finally
            {
                Utility.logExit();
            }
        }

		/// <summary>
		/// Called when a new page is opened. We notify the server.
		/// </summary>
		private void onPageAdded(object sender, Session.Args e)
		{
            if (serverUpdateRequired(e.ActionOrigin) == false) return;
            Utility.logEntry();

			try
			{
				Session session = (Session)sender;
				m_Server.newPage(m_ClientID, session.ID, e.Page.ID);
			}
			catch (Exception ex)
			{
				Logger.log(ex.Message, Logger.Level.WARN);
			}
            finally
            {
                Utility.logExit();
            }
        }

		private void onPageRemoved(object sender, Session.Args e)
		{
            if (serverUpdateRequired(e.ActionOrigin) == false) return;
            Utility.logEntry();

			try
			{
			}
			catch (Exception ex)
			{
				Logger.log(ex.Message, Logger.Level.WARN);
			}
            finally
            {
                Utility.logExit();
            }
        }

		/// <summary>
		/// Called when the active page is changed. We notify the server.
		/// </summary>
		private void onActivePageChanged(object sender, Session.Args e)
		{
            if (serverUpdateRequired(e.ActionOrigin) == false) return;
            if (e.Page == null)
            {
                return;
            }

            Utility.logEntry();

			try
			{
				Session session = (Session)sender;
				m_Server.activePageChanged(m_ClientID, session.ID, e.Page.ID);
			}
			catch (Exception ex)
			{
				Logger.log(ex.Message, Logger.Level.WARN);
			}
            finally
            {
                Utility.logExit();
            }
        }

        #endregion

        #region Private functions

        /// <summary>
        /// Private constructor (for singleton).
        /// </summary>
        private NetworkingClient()
        {
            m_ClientID = Guid.NewGuid();

            // We observe the session so that we can sent updates to the server...
            Session session = Session.getInstance();
            session.ShapeEditCompletedEvent += onShapeUpdated;
            session.ShapeAddedEvent += onShapeUpdated;
            session.ShapeRemovedEvent += onShapeRemoved;
			session.NewSessionEvent += onNewSession;
			session.PageAddedEvent += onPageAdded;
			session.PageRemovedEvent += onPageRemoved;
			session.ActivePageChangedEvent += onActivePageChanged;
        }

		/// <summary>
        /// Returns an ActionOrigin indicating that the action was the result of
        /// a user-action by the client passed in.
        /// </summary>
        private ActionOrigin getActionOrigin(Guid clientID)
        {
            return new ActionOrigin { OriginType = ActionOrigin.OriginTypeEnum.USER_ACTION, ClientID = clientID };
        }

		/// <summary>
		/// Returns true if we should update the server for the event whose
		/// action origin is passed in.
		/// </summary>
		private bool serverUpdateRequired(ActionOrigin actionOrigin)
		{
			// Are we connected to the server?
			if (m_Server == null)
			{
				return false; 
			}

			// Did the action originate remotely?
			if (actionOriginatedRemotely(actionOrigin) == true)
			{
				return false;
			}

			return true;
		}

        /// <summary>
        /// Returns true if the action originated from another client, false otherwise.
        /// </summary>
        private bool actionOriginatedRemotely(ActionOrigin actionOrigin)
        {
            bool bIsRemote = true;

            switch(actionOrigin.OriginType)
            {
                case ActionOrigin.OriginTypeEnum.DB_LOAD:
                    // Actions loaded from the DB are local...
                    bIsRemote = false;
                    break;

                case ActionOrigin.OriginTypeEnum.USER_ACTION:
                    // If it was a user-action, we check if the user was us or someone else...
                    if(actionOrigin.ClientID.CompareTo(m_ClientID) == 0)
                    {
                        bIsRemote = false;
                    }
                    break;
            }

            return bIsRemote;
        }

        /// <summary>
        /// Checks that the active session is the one with the ID passed in.
        /// Throws an exception if it isn't.
        /// </summary>
        private void checkSession(Guid sessionID)
        {
            if (Session.getInstance().ID != sessionID)
            {
                throw new Exception("Active session does not match ID.");
            }
        }

        #endregion

        #region Private data

        private Guid m_ClientID;
        private static NetworkingClient m_Instance = null;
        private IClientToServer m_Server = null;

        #endregion

    }
}
